package www.reagnetbox.com.scan;

import android.content.Context;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbEndpoint;


import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by lgz on 17-4-27.
 */

//
public class UsbPl2303Driver implements CommDriver {
	UsbSerialCommAbility mComm;
	Context mContext;
	UsbCommDev mDev;

	public static final int DEFAULT_READ_BUFFER_SIZE = 16 * 1024;
	public static final int DEFAULT_WRITE_BUFFER_SIZE = 16 * 1024;

	//	public static CommTypeDescript getDescript() {
//		return CommTypeDescript.USB_SERIAL;
//	}
	public UsbPl2303Driver(Context context, CommType dev) {
		mDev = (UsbCommDev) dev;
		mContext = context;
		mComm = new UsbSerialCommAbility(mContext, mDev);
	}

	@Override
	public List<CommTypeDescript> getSupportCommType() {
		List<CommTypeDescript> list = new ArrayList<>();
		list.add(CommTypeDescript.USB_PL2303);
		return list;
	}

	@Override
	public CommAbility getCommAbility() {

		return mComm;
	}


	class UsbSerialCommAbility extends UsbAbility implements UsbSerialConst {
		private static final int USB_READ_TIMEOUT_MILLIS = 1000;
		private static final int USB_WRITE_TIMEOUT_MILLIS = 5000;

		private static final int USB_RECIP_INTERFACE = 0x01;

		private static final int PROLIFIC_VENDOR_READ_REQUEST = 0x01;
		private static final int PROLIFIC_VENDOR_WRITE_REQUEST = 0x01;

		private static final int PROLIFIC_VENDOR_OUT_REQTYPE = UsbConstants.USB_DIR_OUT
				| UsbConstants.USB_TYPE_VENDOR;

		private static final int PROLIFIC_VENDOR_IN_REQTYPE = UsbConstants.USB_DIR_IN
				| UsbConstants.USB_TYPE_VENDOR;

		private static final int PROLIFIC_CTRL_OUT_REQTYPE = UsbConstants.USB_DIR_OUT
				| UsbConstants.USB_TYPE_CLASS | USB_RECIP_INTERFACE;

		private static final int WRITE_ENDPOINT = 0x02;
		private static final int READ_ENDPOINT = 0x83;
		private static final int INTERRUPT_ENDPOINT = 0x81;

		private static final int FLUSH_RX_REQUEST = 0x08;
		private static final int FLUSH_TX_REQUEST = 0x09;

		private static final int SET_LINE_REQUEST = 0x20;
		private static final int SET_CONTROL_REQUEST = 0x22;

		private static final int CONTROL_DTR = 0x01;
		private static final int CONTROL_RTS = 0x02;

		private static final int STATUS_FLAG_CD = 0x01;
		private static final int STATUS_FLAG_DSR = 0x02;
		private static final int STATUS_FLAG_RI = 0x08;
		private static final int STATUS_FLAG_CTS = 0x80;

		private static final int STATUS_BUFFER_SIZE = 10;
		private static final int STATUS_BYTE_IDX = 8;

		private static final int DEVICE_TYPE_HX = 0;
		private static final int DEVICE_TYPE_0 = 1;
		private static final int DEVICE_TYPE_1 = 2;

		private int mDeviceType = DEVICE_TYPE_HX;

		private UsbEndpoint mReadEndpoint;
		private UsbEndpoint mWriteEndpoint;
		private UsbEndpoint mInterruptEndpoint;

		private int mControlLinesValue = 0;

		private int mBaudRate = -1, mDataBits = -1, mStopBits = -1, mParity = -1;

		private int mStatus = 0;
		private volatile Thread mReadStatusThread = null;
		private final Object mReadStatusThreadLock = new Object();
		boolean mStopReadStatusThread = false;
		private IOException mReadStatusException = null;


		private final Object mReadBufferLock = new Object();
		private final Object mWriteBufferLock = new Object();
		protected byte[] mReadBuffer;
		protected byte[] mWriteBuffer;

		private final byte[] inControlTransfer(int requestType, int request,
		                                       int value, int index, int length) throws IOException {
			byte[] buffer = new byte[length];
			int result = deviceConnection.controlTransfer(requestType, request, value,
					index, buffer, length, USB_READ_TIMEOUT_MILLIS);
			if (result != length) {
				throw new IOException(
						String.format("ControlTransfer with value 0x%x failed: %d",
								value, result));
			}
			return buffer;
		}

		private final void outControlTransfer(int requestType, int request,
		                                      int value, int index, byte[] data) throws IOException {
			int length = (data == null) ? 0 : data.length;
			int result = deviceConnection.controlTransfer(requestType, request, value,
					index, data, length, USB_WRITE_TIMEOUT_MILLIS);
			if (result != length) {
				throw new IOException(
						String.format("ControlTransfer with value 0x%x failed: %d",
								value, result));
			}
		}

		private final byte[] vendorIn(int value, int index, int length)
				throws IOException {
			return inControlTransfer(PROLIFIC_VENDOR_IN_REQTYPE,
					PROLIFIC_VENDOR_READ_REQUEST, value, index, length);
		}

		private final void vendorOut(int value, int index, byte[] data)
				throws IOException {
			outControlTransfer(PROLIFIC_VENDOR_OUT_REQTYPE,
					PROLIFIC_VENDOR_WRITE_REQUEST, value, index, data);
		}

		private void resetDevice() throws IOException {
			purgeHwBuffers(true, true);
		}

		private final void ctrlOut(int request, int value, int index, byte[] data)
				throws IOException {
			outControlTransfer(PROLIFIC_CTRL_OUT_REQTYPE, request, value, index,
					data);
		}

		private void doBlackMagic() throws IOException {
			vendorIn(0x8484, 0, 1);
			vendorOut(0x0404, 0, null);
			vendorIn(0x8484, 0, 1);
			vendorIn(0x8383, 0, 1);
			vendorIn(0x8484, 0, 1);
			vendorOut(0x0404, 1, null);
			vendorIn(0x8484, 0, 1);
			vendorIn(0x8383, 0, 1);
			vendorOut(0, 1, null);
			vendorOut(1, 0, null);
			vendorOut(2, (mDeviceType == DEVICE_TYPE_HX) ? 0x44 : 0x24, null);
		}

		private void setControlLines(int newControlLinesValue) throws IOException {
			ctrlOut(SET_CONTROL_REQUEST, newControlLinesValue, 0, null);
			mControlLinesValue = newControlLinesValue;
		}

		private final void readStatusThreadFunction() {
			try {
				while (!mStopReadStatusThread) {
					byte[] buffer = new byte[STATUS_BUFFER_SIZE];
					int readBytesCount = deviceConnection.bulkTransfer(mInterruptEndpoint,
							buffer,
							STATUS_BUFFER_SIZE,
							500);
					if (readBytesCount > 0) {
						if (readBytesCount == STATUS_BUFFER_SIZE) {
							mStatus = buffer[STATUS_BYTE_IDX] & 0xff;
						} else {
							throw new IOException(
									String.format("Invalid CTS / DSR / CD / RI status buffer received, expected %d " +
													"bytes, but received %d",
											STATUS_BUFFER_SIZE,
											readBytesCount));
						}
					}
				}
			} catch (IOException e) {
				mReadStatusException = e;
			}
		}

		private final int getStatus() throws IOException {
			if ((mReadStatusThread == null) && (mReadStatusException == null)) {
				synchronized (mReadStatusThreadLock) {
					if (mReadStatusThread == null) {
						byte[] buffer = new byte[STATUS_BUFFER_SIZE];
						int readBytes = deviceConnection.bulkTransfer(mInterruptEndpoint,
								buffer,
								STATUS_BUFFER_SIZE,
								100);
						if (readBytes != STATUS_BUFFER_SIZE) {
							LogComm.printf("Could not read initial CTS / DSR / CD / RI status");
						} else {
							mStatus = buffer[STATUS_BYTE_IDX] & 0xff;
						}

						mReadStatusThread = new Thread(new Runnable() {
							@Override
							public void run() {
								readStatusThreadFunction();
							}
						});
						mReadStatusThread.setDaemon(true);
						mReadStatusThread.start();
					}
				}
			}

            /* throw and clear an exception which occured in the status read thread */
			IOException readStatusException = mReadStatusException;
			if (mReadStatusException != null) {
				mReadStatusException = null;
				throw readStatusException;
			}

			return mStatus;
		}

		private final boolean testStatusFlag(int flag) throws IOException {
			return ((getStatus() & flag) == flag);
		}

		public int read(byte[] dest, int timeoutMillis) throws IOException {
			synchronized (mReadBufferLock) {
				int readAmt = Math.min(dest.length, mReadBuffer.length);

				LogComm.printf("read pl2303 begin");
				int numBytesRead = deviceConnection.bulkTransfer(mReadEndpoint, mReadBuffer,
						readAmt, timeoutMillis);
				LogComm.printf("read pl2303 end:" + numBytesRead);
				if (numBytesRead < 0) {
					return 0;
				}
				System.arraycopy(mReadBuffer, 0, dest, 0, numBytesRead);
				return numBytesRead;
			}
		}

		public int write(byte[] src, int timeoutMillis) throws IOException {
			int offset = 0;

			while (offset < src.length) {
				final int writeLength;
				final int amtWritten;

				synchronized (mWriteBufferLock) {
					final byte[] writeBuffer;

					writeLength = Math.min(src.length - offset, mWriteBuffer.length);
					if (offset == 0) {
						writeBuffer = src;
					} else {
						// bulkTransfer does not support offsets, make a copy.
						System.arraycopy(src, offset, mWriteBuffer, 0, writeLength);
						writeBuffer = mWriteBuffer;
					}

					LogComm.printf("wite pl2303 begin");
					amtWritten = deviceConnection.bulkTransfer(mWriteEndpoint,
							writeBuffer, writeLength, timeoutMillis);
					LogComm.printf("wite pl2303 end:" + amtWritten);
				}

				if (amtWritten <= 0) {
					throw new IOException("Error writing " + writeLength
							+ " bytes at offset " + offset + " length="
							+ src.length);
				}

				offset += amtWritten;
			}
			return offset;
		}

		public void setParameters(int baudRate, int dataBits, int stopBits,
		                          int parity) throws IOException {
			if ((mBaudRate == baudRate) && (mDataBits == dataBits)
					&& (mStopBits == stopBits) && (mParity == parity)) {
				// Make sure no action is performed if there is nothing to change
				return;
			}

			byte[] lineRequestData = new byte[7];

			lineRequestData[0] = (byte) (baudRate & 0xff);
			lineRequestData[1] = (byte) ((baudRate >> 8) & 0xff);
			lineRequestData[2] = (byte) ((baudRate >> 16) & 0xff);
			lineRequestData[3] = (byte) ((baudRate >> 24) & 0xff);

			switch (stopBits) {
				case STOPBITS_1:
					lineRequestData[4] = 0;
					break;

				case STOPBITS_1_5:
					lineRequestData[4] = 1;
					break;

				case STOPBITS_2:
					lineRequestData[4] = 2;
					break;

				default:
					throw new IllegalArgumentException("Unknown stopBits value: " + stopBits);
			}

			switch (parity) {
				case PARITY_NONE:
					lineRequestData[5] = 0;
					break;

				case PARITY_ODD:
					lineRequestData[5] = 1;
					break;

				case PARITY_EVEN:
					lineRequestData[5] = 2;
					break;

				case PARITY_MARK:
					lineRequestData[5] = 3;
					break;

				case PARITY_SPACE:
					lineRequestData[5] = 4;
					break;

				default:
					throw new IllegalArgumentException("Unknown parity value: " + parity);
			}

			lineRequestData[6] = (byte) dataBits;

			ctrlOut(SET_LINE_REQUEST, 0, 0, lineRequestData);

			resetDevice();

			mBaudRate = baudRate;
			mDataBits = dataBits;
			mStopBits = stopBits;
			mParity = parity;
		}

		public boolean getCD() throws IOException {
			return testStatusFlag(STATUS_FLAG_CD);
		}

		public boolean getCTS() throws IOException {
			return testStatusFlag(STATUS_FLAG_CTS);
		}

		public boolean getDSR() throws IOException {
			return testStatusFlag(STATUS_FLAG_DSR);
		}

		public boolean getDTR() throws IOException {
			return ((mControlLinesValue & CONTROL_DTR) == CONTROL_DTR);
		}

		public void setDTR(boolean value) throws IOException {
			int newControlLinesValue;
			if (value) {
				newControlLinesValue = mControlLinesValue | CONTROL_DTR;
			} else {
				newControlLinesValue = mControlLinesValue & ~CONTROL_DTR;
			}
			setControlLines(newControlLinesValue);
		}

		public boolean getRI() throws IOException {
			return testStatusFlag(STATUS_FLAG_RI);
		}

		public boolean getRTS() throws IOException {
			return ((mControlLinesValue & CONTROL_RTS) == CONTROL_RTS);
		}

		public void setRTS(boolean value) throws IOException {
			int newControlLinesValue;
			if (value) {
				newControlLinesValue = mControlLinesValue | CONTROL_RTS;
			} else {
				newControlLinesValue = mControlLinesValue & ~CONTROL_RTS;
			}
			setControlLines(newControlLinesValue);
		}

		public boolean purgeHwBuffers(boolean purgeReadBuffers, boolean purgeWriteBuffers) throws IOException {
			if (purgeReadBuffers) {
				vendorOut(FLUSH_RX_REQUEST, 0, null);
			}

			if (purgeWriteBuffers) {
				vendorOut(FLUSH_TX_REQUEST, 0, null);
			}

			return purgeReadBuffers || purgeWriteBuffers;
		}

		public UsbSerialCommAbility(Context context, UsbCommDev dev) {
			super(context, dev);
			mReadBuffer = new byte[DEFAULT_READ_BUFFER_SIZE];
			mWriteBuffer = new byte[DEFAULT_WRITE_BUFFER_SIZE];
		}


		@Override
		public void open() throws IOException {

			try {
				super.open();
				if (usbInterface == null || usbDevice == null) {
					throw new IOException("not find pl2303 usb");
				}

//				if (!connection.claimInterface(usbInterface, true)) {
//					throw new IOException("Error claiming Prolific interface 0");
//				}
//
//				mConnection = connection;
				boolean opened = false;
				for (int i = 0; i < usbInterface.getEndpointCount(); ++i) {
					UsbEndpoint currentEndpoint = usbInterface.getEndpoint(i);

					switch (currentEndpoint.getAddress()) {
						case READ_ENDPOINT:
							mReadEndpoint = currentEndpoint;
							break;

						case WRITE_ENDPOINT:
							mWriteEndpoint = currentEndpoint;
							break;

						case INTERRUPT_ENDPOINT:
							mInterruptEndpoint = currentEndpoint;
							break;
					}
				}

				if (usbDevice.getDeviceClass() == 0x02) {
					mDeviceType = DEVICE_TYPE_0;
				} else {
					try {
						Method getRawDescriptorsMethod
								= deviceConnection.getClass().getMethod("getRawDescriptors");
						byte[] rawDescriptors
								= (byte[]) getRawDescriptorsMethod.invoke(deviceConnection);
						byte maxPacketSize0 = rawDescriptors[7];
						if (maxPacketSize0 == 64) {
							mDeviceType = DEVICE_TYPE_HX;
						} else if ((usbDevice.getDeviceClass() == 0x00)
								|| (usbDevice.getDeviceClass() == 0xff)) {
							mDeviceType = DEVICE_TYPE_1;
						} else {
							LogComm.printf("Could not detect PL2303 subtype, "
									+ "Assuming that it is a HX device");
							mDeviceType = DEVICE_TYPE_HX;
						}
					} catch (NoSuchMethodException e) {
						LogComm.printf("Method UsbDeviceConnection.getRawDescriptors, "
								+ "required for PL2303 subtype detection, not "
								+ "available! Assuming that it is a HX device");
						mDeviceType = DEVICE_TYPE_HX;
						throw new IOException(e);
					} catch (Exception e) {
						LogComm.printf("An unexpected exception occured while trying "
								+ "to detect PL2303 subtype" + e);
						throw new IOException(e);
					}
				}

				setControlLines(mControlLinesValue);
				resetDevice();
				setParameters(mDev.getBaudrate(), DATABITS_8,
						STOPBITS_1, PARITY_NONE);
				setDTR(true);
//				setFlowcontrol(FTDI232R_SIO_RTS_CTS_HS);
				doBlackMagic();
				opened = true;

//				finally {
//					if (!opened) {
//						deviceConnection = null;
//						deviceConnection.releaseInterface(usbInterface);
//					}
//				}
			} catch (IOException e) {
				throw e;
			}
		}


		@Override
		public void close() throws IOException {

			if (deviceConnection == null) {
				throw new IOException("Already closed");
			}
			try {
				mStopReadStatusThread = true;
				synchronized (mReadStatusThreadLock) {
					if (mReadStatusThread != null) {
						try {
							mReadStatusThread.join();
						} catch (Exception e) {
							LogComm.printf("An error occured while waiting for status read thread" + e);
							throw new IOException(e);
						}
					}
				}
				resetDevice();
			} catch (IOException ex) {
			} finally {
				deviceConnection.releaseInterface(usbInterface);
				super.close();
			}
		}

		@Override
		public int sendDataBlock(byte[] data, int timeOut) {
			try {
				return write(data, timeOut);

			} catch (IOException e) {
				e.printStackTrace();
			}
			return -1;
		}


		@Override
		public byte[] recvDataLock(int timeOut) {
			byte[] buf = new byte[0x2000];
			try {
				int len = read(buf, timeOut);
				if (len > 0) {

					byte[] data = new byte[len];
					System.arraycopy(buf, 0, data, 0, len);
					return data;
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
			return null;
		}


	}
}
